本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本篇文章請搭配
[5-1] 環域與繪圖工具 - 以Leaflet Draw實現
[5-2] Callback & Promise - 解決request非同步的四種解法
讓我們回顧前天講到的繪圖工具,根據畫不同的圖形,
而有不同後續處理的方式,並且用if...else...去判斷。
        LMap.on(L.Draw.Event.CREATED, function (e) {
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);
            //console.log(type)
            //console.log(arguments)
            if (type === 'circle') {
                var center = layer.getLatLng();
                var radius = layer.getRadius();
                console.log(`經度: ${center.lng}, 緯度: ${center.lat}`);
                console.log(`半徑: ${radius} (m)`);
                
            } else if (type === 'marker') {
                var point = layer.getLatLng();
                console.log(`經度: ${point.lng}, 緯度: ${point.lat}`);
                
            } else if (type === 'rectangle') {
                var str = "";
                var arr = layer.getLatLngs();
                arr = arr[0].forEach(function (item, index) {
                    str += `${index} => 經度: ${item.lng}, 緯度: ${item.lat}`
                });
                console.log(e.layer.toGeoJSON());
                console.log(str);
            } else if (type === 'polygon') {
                var str = "";
                var arr = layer.getLatLngs();
                arr = arr[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });
                console.log(arr);
            }
        });
如果後續要做的事情很多,
昨天有教大家callback或ES6 Promise的方法,將程式分離出去。
可是根據不同的幾何圖形,每次都要重新看一次callback的arguments,
並且從中找到可以使用的資料。如果今天API突然改版,資料回傳的格式變了呢?如果今天老闆說畫圓形要用這個API,可是要用另一個API提供的畫扇形,要怎麼整合並因應不同API資料儲存格式的差異?
就讓我們用Adapter Design Pattern試著來重構吧!
↓ 首先先建立一個繪圖器類別,由它建立的物件可以拿來繪圖。
        function Drawing(map, drawingMode, option = {}) {
            if (!(this instanceof Drawing)) {  // 如果沒有用new呼叫,幫他new
                return new Drawing(map, drawingMode, option);
            }
            this.map = map;
            this.drawingMode = drawingMode;
            this.option = option;
        }
        Drawing.prototype.Start = function (completeCallback) {
            const el = this;
            if (L.Draw[this.drawingMode] instanceof Function) {
                this.drawing = new L.Draw[this.drawingMode](this.map, this.option);
                this.map.off('draw:created').on('draw:created', function (e) {
                    e.layer.addTo(el.map);
                    completeCallback(e);
                });
                this.drawing.enable();
            } else {
                console.log(new Error('invalid drawingMode.'));
            }
        }
↑ 建立一個原型方法Start,繪圖開始。
這裡有幾個重點
配接器模式用以解決兩個介面不相容的問題,就好像是萬用插座一樣,不論是兩孔、三孔還是八字型插頭都可以輕鬆轉接。而建立這個配接器(轉接頭),首先要先定義通用的介面。
// 以物件陣列方式儲存點座標
var pointList = [{ x: 121.5, y: 24 }, { x: 121.2, y: 23.8 }, { x: 121, y: 23.5 }];
// 以物件的方式儲存中心點及圓心
var obj = { x: 121, y: 23, radius: 1000 };
// Geojson
定義完通用介面後,接者寫轉接的方式,針對不同的介面進行轉接,轉成通用介面。這些轉接的方法稱為Adaptee,未來有新的介面出現時,只要寫新的Adaptee就能輕鬆轉接。
        var adaptee = {  // 配接器,轉換不同型式介面
            // TGOS.TGLine 轉為 pointList
            TGLineToPointList: function (e) {  
                console.log(e.overlay.getPath());
                var pointList = e.overlay.getPath().path.map(function (item) {
                    return {
                        x: item.x,
                        y: item.y
                    }
                });
                return pointList;
            },
            // TGOS.TGPolygon 轉為 pointList
            TGPolygonToPointList: function (e) {  
                console.log(e.overlay.getPath().rings_[0].linestring);
                var pointList = e.overlay.getPath().rings_[0]
                        .linestring.path.map(function (item) {
                    return {
                        x: item.x,
                        y: item.y
                    }
                });
                return pointList;
            },
            // TGOS.TGCircle 轉為 Object
            TGCircleToObject: function (e) {  
                var tgCircle = e.overlay.getPath();
                return {
                    x: tgCircle.getCenter().x,
                    y: tgCircle.getCenter().y,
                    radius: tgCircle.getRadius()
                };
            },
            // Leaflet Circle 轉為 Object
            LCircleToObject: function (e) {  
                var layer = e.layer;
                return {
                    x: layer.getLatLng().lng,
                    y: layer.getLatLng().lat,
                    radius: layer.getRadius()
                };
            },
            // Leaflet Polygon 轉為 pointList
            LPolygonToPointList: function (e) {  
                var pointList = e.layer.getLatLngs();
                pointList = pointList[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });
                return pointList;
            },
            // Leaflet marker 轉為 Object
            LMarkerToObject: function (e) {  
                return {
                    x: e.layer.getLatLng().lng,
                    y: e.layer.getLatLng().lat,
                };
            },
            // Leaflet Rectangle 轉為 pointList
            LRectangleToPointList: function (e) {  
                var pointList = e.layer.getLatLngs();
                pointList = pointList[0].map(function (item, index) {
                    return {
                        x: item.lng,
                        y: item.lat
                    }
                });
                return pointList;
            },
            // Leaflet Rectangle 轉為 Geojson
            LRectangleToGeojson: function (e) {  
                return e.layer.toGeoJSON();
            },
        };
建立好Adaptee後,接著新增共用的轉接方法來使用。
        Drawing.prototype.StartWithAdapter = function (adapter, completeCallback) {
            adapter = adapter instanceof Function ? 
                        adapter : function (e) {
                console.log(new TypeError('adapter is not a Function.'));
                return e;
            }
            this.Start(function (e) {
                completeCallback(adapter(e));
            });
        }
建立Drawing的原型方法StartWithAdapter
先判斷adaptee是否為Function,如果是才繼續動作。
呼叫this.Start(),會呼叫到剛剛建立的原型方法Start,並且把completeCallback函式傳入。
1. 先用沒有adaper的方法呼叫
        var drawing = new Drawing(LMap, 'Circle', {});
        drawing.Start(function () {
            console.log(arguments)
        })
↓ 畫圓
↓ 結果
可以看到callback function回傳的是Leaflet的物件,就還需額外處理這個物件取出我們要的資訊。
2. 使用adaper的方法呼叫
        drawing.StartWithAdapter(adaptee.LCircleToObject, function () {
            console.log(arguments)
        });
↓ 結果
回傳結果為我們定義的介面,有經度、緯度以及半徑。
既然昨天介紹了ES6 Promise物件,那我們就改用Promise的方式來改寫配接器模式吧!
↓ Adaptee不變,首先先來改寫Start方法。
        Drawing.prototype.Start = function () {
            const el = this;
            return new Promise(function (resolve, reject) {
                if (L.Draw[el.drawingMode] instanceof Function) {
                    el.drawing = new L.Draw[el.drawingMode](el.map, el.option);
                    el.map.off('draw:created').on('draw:created', function (e) {
                        e.layer.addTo(el.map);
                        resolve(e);
                    });
                    el.drawing.enable();
                } else {
                    console.log(new Error('invalid drawingMode.'));
                    reject(new Error('invalid drawingMode.'));
                }
            });
        }
讓Drawing的原型方法Start改為return一個Promise物件,並且加入resolve(e)、reject('error')來處理成功以及失敗的回調函式。注意!這邊要用const el = this;先把this寫入一個變數,否則在Promise內的this會指向Promise物件。
        Drawing.prototype.StartWithAdapter = function (adapter) {
            const el = this;
            return new Promise(function (resolve, reject) {
                adapter = adapter instanceof Function ? adapter : function (e) {
                    console.log(new TypeError('adapter is not a Function.'));
                    return e;
                }
                el.Start().then((e) => resolve(adapter(e)));
            });
        }
Drawing的原型方法StartWithAdapter也return一個Promise物件,並在裡面呼叫原型方法Start,並用.then接續進行執行adapter的回呼。
1. 首先先用沒有adaper的方法呼叫
        var drawing = new Drawing(LMap, 'Rectangle', {});
        drawing.Start().then(res => {
            console.log(1);
            return res;
        }).then(res => {
            console.log(2);
            return res;
        }).then(res => console.log(res));
↓ 畫長方形
↓ 結果
可以看到Promise物件會依照.then依序進行後續動作;沒有使用adapter回傳的是Leaflet的物件,還需額外處理這個物件取出我們要的資訊。
2. 使用adaper的方法呼叫
↓ 一行解決,乾淨俐落
drawing.StartWithAdapter(adaptee.LRectangleToGeojson).then(res => console.log(res));
↓ 結果
回傳為我們指定的Geojson格式。
今天簡單介紹了配接器模式,
配接器模式最著名的其實是WebRTC,
大家有空可以去 github 拜讀一下大神們寫的Source Code。
明天繼續努力!![]()